package roborally.itemsonboard;

import java.util.Random;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;
import roborally.board.Board;
import roborally.factory.PositionFactory;
import roborally.shoot.BeenShot;
import roborally.supporting.DefaultConstants;
import roborally.supporting.PhysicalAmount;
import roborally.supporting.Position;
import roborally.supporting.Unit;
import roborally.usableitems.ItemWithWeight;
import roborally.usableitems.UsableWithBoard;

/**
 * A surpriseBox is a BoardItemImpl that can be used. When a robot uses a surpriseBox, a random
 * behavior will be executed. When a surpriseBox is been hit, it explodes. All the neighbors
 * are hit as well.
 * 
 * @invar The boardItem must have a valid position at all times. (repeated)
 * 		| canHaveAsPosition(this.getPosition())
 * @invar The boardItem must have a proper board at all times. (repeated)
 * 		| hasProperBoard()
 * @invar If this item is possessed by a robot, then the board must be null. (repeated)
 * 		| !this.isPossessed() || this.getBoard() == null
 * 
 * @invar The weight must be valid.
 * 		| isValidWeight(this.getWeight())
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 4.0
 */
public class SurpriseBox extends BoardItemImpl implements UsableWithBoard {

//Constructor ----------------------------------------------------------------------------------------------------------	
	
	/**
	 * Creates a new surpiseBox with the given options.
	 * 
	 * @param weight The weight of the surpriseBox.
	 * @param board The board of the supriseBox.
	 * @param position The position of the surpriseBox.
	 * @effect The board and position will be set to the given values if possible.
	 * 		| super(board, position)
	 * @post The weight is set to the given value if valid otherwise the default value is set.
	 * 		| if !isValidWeigh(weight)
	 * 		| 	then (new this).getWeight() == DefaultConstants.SURPISE_BOX_WEIGHT
	 * 		| else
	 * 		| 	(new this).getWeigh() == weight
	 * @post
	 * 		| (new this).isPossessed() == false
	 * @throws IllegalArgumentException (repeated)
	 * 		Position is null
	 * 		| position== null
	 * @throws IllegalArgumentException (repeated)
	 * 		When the board is not null and can not contain this item.
	 * 		| board != null && !board.canHaveAsItem(new this)
	 */
	public SurpriseBox(PhysicalAmount weight, Board board, Position position)
			throws IllegalArgumentException {
		super(board, position);
		if(!isValidWeight(weight))
			weight = DefaultConstants.SURPISE_BOX_WEIGHT;
		this.weight = weight;
		this.setPossessed(false);
	}
	
	/**
	 * Creates a new supriseBox with no board and position Origin.
	 * 
	 * @param weight The weight of the surpiseBox.
	 * @effect Creates a new supriseBox with no board and position Origin and given weight.
	 * 		| this(weight, null, Position.Origin)
	 */
	public SurpriseBox(PhysicalAmount weight) throws IllegalArgumentException {
		this(weight, null, Position.ORIGIN);
	}
	
	/**
	 * Creates a default surpiseBox.
	 * 
	 * @effect Creates a default surpriseBox.
	 * 		| this(DefaultConstans.SUPRISE_BOX_WEIGHT)
	 */
	public SurpriseBox() {
		this(DefaultConstants.SURPISE_BOX_WEIGHT);
	}
	
//Position --------------------------------------------------------------------------------------	

	/**
	 * Checks whether this surpriseBox can share a position with the given boardItem.
	 * 
	 * @param boardItem The item which is at the same position as this surpriseBox.
	 * @return False if boardItem is null. (repeated)
	 * 		| if boardItem == null
	 * 		|	then result == false
	 * @return True if boardItem is the same as this. (repeated)
	 * 		| if positionItem == this
	 * 		| 	then result == true
	 * @return False if boardItem is a Wall.
	 * 		| if Wall.class.isInstance(boardItem)
	 *		|	then result == false
	 * @return True if boardItem is a battery.
	 * 		| if Battery.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a repairKit.
	 * 		| if RepairKit.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a surpriseBox.
	 * 		| if SurpriseBox.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a robot.
	 * 		| if Robot.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return When another type of boardItem is created, in the future, he is responsible for handling this.
	 * 		| result == boardItem.canHaveItemOnSamePosition(this)
	 */
	@Override
	public boolean canHaveItemOnSamePosition(BoardItem boardItem) {
		if(boardItem == null)
			return false;
		else if(Wall.class.isInstance(boardItem))
			return false;
		else if(Battery.class.isInstance(boardItem))
			return true;
		else if(RepairKit.class.isInstance(boardItem))
			return true;
		else if(SurpriseBox.class.isInstance(boardItem))
			return true;
		else if(Robot.class.isInstance(boardItem))
			return true;
		else
			return boardItem.canHaveItemOnSamePosition(this);
	}

//Shoot ----------------------------------------------------------------------------------------------------------
	
	/**
	 * When a surpriseBox is been shot, it explodes an hit all the neighbors.
	 * @pre
	 * 		| this.getBoard() != null
	 * @pre
	 * 		| !this.isTerminated()
	 * @effect
	 * 		| this.isTerminated()
	 * @effect
	 * 		| for each position in {this.getPosition().getLegalNeighbors()}
	 * 		|	for each beenShot in {(old this).getBoard().getItemsAt(position)}
	 * 		|		beenShot.beenShot()
	 */
	@Override
	public void beenShot() {
		assert this.getBoard() != null;
		assert !this.isTerminated();
		//we use copies if the behavior of terminate changes.
		Position tempP = this.getPosition();
		Board tempB = this.getBoard();
		//It is very important in case of a cascade effect that the surprise-box is FIRST!!!!!! terminated and then explodes. Otherwise an infinite loop occurs.
		this.terminate(); 			
		for(Position lusPosition: tempP.getLegalNeighbors())
			for(BeenShot lusBeenShot : tempB.getItemsAt(lusPosition))
				lusBeenShot.beenShot();
	}

//Weight ----------------------------------------------------------------------------------------------------------	

	/**
	 * Returns the weight of this supriseBox.
	 */
	@Override 
	@Basic @Raw @Immutable
	public PhysicalAmount getWeight() {
		return this.weight;
	}
	
	/**
	 * Compares the item with weight to this one.
	 * 
	 * @param item The item with which this must be compared.
	 * @return (repeated)
	 * 		| result == this.getWeight().compareTo(item.getWeight())
	 * @throws IllegalArgumentException (repeated)
	 * 		| usable == null
	 */
	@Override
	public int compareTo(ItemWithWeight item) throws IllegalArgumentException{
		if(item == null)
			throw new IllegalArgumentException("Cannot compare to null");
		return this.getWeight().compareTo(item.getWeight());
	}
	
	/**
	 * Checks the weight.
	 * 
	 * @param weight The weight to be checked.
	 * @return False if the weight is null
	 * 		| if weight == null
	 * 		|	then result == false
	 * @return False if the unit of the weight can not converted to Unit.GRAM
	 * 		| if !weight.getUnit().canConvertTo(Unit.GRAM)
	 * 		|	then result == false
	 * @return False if the weight amount is negative.
	 * 		| if weight.getAmount() < 0
	 * 		|	then result == false.
	 */
	@Raw
	public static boolean isValidWeight(PhysicalAmount weight){
		if(weight == null)
			return false;
		else if(!weight.getUnit().canConvertTo(Unit.GRAM))
			return false;
		else if(weight.getAmount() < 0)
			return false;
		return true;
	}
	
	/**
	 * The weight of this surpriseBox.
	 */
	private final PhysicalAmount weight;


//Usable ----------------------------------------------------------------------------------------------------------	
	
	/**
	 * Let the robot pick up this surpriseBox, if possible.
	 * 
	 * @param robot The robot that should pick up this surpriseBox.
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre  (repeated)
	 * 		| robot.canPickUp(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @pre (repeated)
	 * 		| !isPossessed()
	 * @effect (repeated)
	 * 		| robot.getBoard().removeAsItem(this)
	 */
	 @Override
	public void isPickedUp(Robot robot) {
		assert robot != null;
		assert robot.canPickUp(this);
		assert !isTerminated();
		assert !isPossessed();
		robot.getBoard().removeAsItem(this);
	}
	
	/**
	 * Let the robot drop this surpriseBox, if possible.
	 *  
	 * @param robot The robot that should drop this surpriseBox.
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre (repeated)
	 * 		| robot.canDrop(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @pre (repeated)
	 * 		| !isPossessed()
	 * @effect (repeated)
	 * 		| if robot.getBoard() != null
	 * 		|	then robot.getBoard().addAsItem(this)
	 * @effect (repeated)
	 * 		| if robot.getBoard() == null
	 * 		|	then this.setBoard(null)
	 * @post (repeated)
	 * 		| (new this).getPosition() == robot.getPosition()
	 * 
	 * @note The robot will leave this method in a raw state because this battery will have a board and
	 * a position, and the robot will possess this surpriseBox.
	 */
	@Override
	@Raw 
	public void isDropped(Robot robot) {
		assert robot != null;
		assert robot.canDrop(this);
		assert !isTerminated();
		assert !isPossessed();
		this.setPosition(robot.getPosition());
		if(robot.getBoard()!=null)
			robot.getBoard().addAsItem(this);
		else
			this.setBoard(null);
	}
	
	/**
	 * A random behavior of the possible behaviors of a surpriseBox is executed.
	 * 
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre (repeated)
	 * 		| robot.canUse(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @effect
	 * 		| Behaviour.values()[(new Random()).nextInt(Behaviour.values().length)].execute(robot, this)
	 */
	@Override
	public void isUsed(Robot robot) {
		assert robot != null;
		assert robot.canUse(this);
		assert !isTerminated();
		Behavior.values()[(new Random()).nextInt(Behavior.values().length)].execute(robot, this);
	}
	
	/**
	 * Returns whether this surpriseBox is possessed by a robot or not.
	 */
	@Override
	@Basic @Raw 
	public boolean isPossessed(){
		return possessed;
	}
	
	/**
	 * Sets whether this surpriseBox is possessed by a robot or not.
	 * 
	 * @param value The value to be set.
	 * @post (repeated)
	 * 		| (new this).isPossessed() == value
	 */
	@Override
	public void setPossessed(boolean value){
		possessed = value;
	}
	
	private boolean possessed;
		
//Object ---------------------------------------------------------------------------------------------------
	
	/**
	 * Returns a textual specification of this surpriseBox.
	 * 
	 * @return A textual specification of this surpriseBox.
	 * 		| result == super.toString() + ", [Weight: " + this.getWeight() + "]"
	 */
	@Override
	public String toString() {
		return super.toString() + ", [Weight: " + this.getWeight() + "]";

	}
	
//LegalStatus ---------------------------------------------------------------------------------------------------
	
	/**
	 * Controls if the surpriseBox satisfies all his class invariants.
	 * 
	 * @return False if the board is not valid. (repeated)
	 * 		| if !hasProperBoard()
	 * 		|	then result == false
	 * @return False if the position is not legal. (repeated)
	 * 		| if !canHaveAsPosition(this.getPosition())
	 * 		|	then result == false
	 * 
	 * @return False if the weight is not valid.
	 * 		| if !SurpriseBox.isValidWeight(this.getWeight())
	 * 		|	then result == false
	 * @return False if the surpriseBox is possessed and has a board.
	 * 		| if this.isPossessed() && this.getBoard() != null
	 * 		|	then result == false
	 */	
	@Override
	public boolean legalStatus() {
		return super.legalStatus() && SurpriseBox.isValidWeight(getWeight()) && (!this.isPossessed() || this.getBoard() == null);
	}
	
//Behavior --------------------------------------------------------------------------------------------------------------	
	
	/**
	 * A enum representing the different behaviors of a supriseBox when used.
	 * 
	 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
	 * @version 2.0
	 * 
	 * @note The only reason why this enum is public and not private, is the fact that we can test the methods separately.
	 */
	public enum Behavior {
		EXPLODE{

			/**
			 * A supriseBox with this behavior will explode. The robot is been hit.
			 * 
			 * @pre (repeated)
			 * 		| robot != null
			 * @pre (repeated)
			 * 		| robot.canUse(surpriseBox)
			 * @pre (repeated)
			 * 		| !surpriseBox.isTerminated()
			 * @effect
			 * 		| robot.beenShot()
			 * @effect (repeated)
			 * 		| surpriseBox.terminate()	
			 */
			@Override
			public void execute(Robot robot, SurpriseBox surpriseBox) {
				assert robot != null;
				assert robot.canUse(surpriseBox);
				assert !surpriseBox.isTerminated();
				robot.beenShot();
				super.execute(robot, surpriseBox);
			}
			
		},
		TELEPORT{
			
			/**
			 * The robot will move towards a random position. If the robot is located on a board,
			 * he will move towards a random, valid position on this board. Else, he will move 
			 * towards a random position.
			 * 
			 * @pre (repeated)
			 * 		| robot != null
			 * @pre (repeated)
			 * 		| robot.canUse(surpriseBox)
			 * @pre (repeated)
			 * 		| !surpriseBox.isTerminated()
			 * @post
			 * 		| if robot.getBoard() == null
			 * 		|	then (new robot).getPosition().equals( random{position | Position.isInstance(position)}
			 * @post
			 * 		| if robot.getBoard() != null
			 * 		|	then (new robot).getPosition().equals( random{position | Position.isInstance(position) && robot.canHaveAsPosition(position) }
			 * @effect (repeated)
			 * 		| surpriseBox.terminate()
			 */
			@Override
			public void execute(Robot robot, SurpriseBox surpriseBox) {
				assert robot != null;
				assert robot.canUse(surpriseBox);
				assert !surpriseBox.isTerminated();

				Random randomGenerator = new Random();
				if(robot.getBoard() == null)
					robot.setPosition(PositionFactory.get().create(Math.abs(randomGenerator.nextLong()),Math.abs(randomGenerator.nextLong())));
				else {
					Long x = Math.round(randomGenerator.nextDouble() * robot.getBoard().getXLimit());
					Long y = Math.round(randomGenerator.nextDouble() * robot.getBoard().getYLimit());
					Position position = PositionFactory.get().create(x, y);
					while(!robot.canHaveAsPosition(position)) {
						x = Math.round(randomGenerator.nextDouble() * robot.getBoard().getXLimit());
						y = Math.round(randomGenerator.nextDouble() * robot.getBoard().getYLimit());
						position = PositionFactory.get().create(x, y);
					}
					robot.setPosition(PositionFactory.get().create(x, y));
				}
				super.execute(robot, surpriseBox);				
			}
			
		},
		NEW_USABLE{

			/**
			 * Adds a random usable to the usables of the robot.
			 * 
			 * @pre (repeated)
			 * 		| robot != null
			 * @pre (repeated)
			 * 		| robot.canUse(surpriseBox)
			 * @pre (repeated)
			 * 		| !surpriseBox.isTerminated()
			 * @effect
			 * 		| let randomGenerator = new Random()
			 * 		| let number = randomGenerator.nextInt(3)
			 * 		| let usable = null
			 * 		| if number == 0
			 * 		|	then usable = new SurpriseBox(DefaultConstants.SURPISE_BOX_WEIGHT.multiply(randomGenerator.nextDouble()), null, robot.getPosition())
			 * 		| else if number == 1
			 * 		|		then usable = new Battery(DefaultConstants.BATTERY_WEIGHT.multiply(randomGenerator.nextDouble()),
			 *		|						DefaultConstants.BATTERY_ENERGY_LIMIT.multiply(randomGenerator.nextDouble()), null, robot.getPosition() )
			 * 		| else if number == 2
			 * 		|		then usable = new RepairKit(DefaultConstants.REPAIR_KIT_WEIGHT.multiply(randomGenerator.nextDouble()),
			 *		|						DefaultConstants.REPAIR_KIT_ENERGY_LIMIT.multiply(randomGenerator.nextDouble()), null, robot.getPosition())
			 *		| robot.addAsUsableItem(usable)
			 * @effect (repeated)
			 * 		| surpriseBox.terminate()
			 */
			@Override
			public void execute(Robot robot, SurpriseBox surpriseBox) {
				assert robot != null;
				assert robot.canUse(surpriseBox);
				assert !surpriseBox.isTerminated();
				
				Random randomGenerator = new Random();
				UsableWithBoard usable;
				switch(randomGenerator.nextInt(3)) {
				case 0:
					usable = new SurpriseBox(DefaultConstants.SURPISE_BOX_WEIGHT.multiply(randomGenerator.nextDouble()), null, robot.getPosition());
					break;
				case 1:
					usable = new Battery(DefaultConstants.BATTERY_WEIGHT.multiply(randomGenerator.nextDouble()),
							DefaultConstants.BATTERY_ENERGY_LIMIT.multiply(randomGenerator.nextDouble()), null, robot.getPosition() );
					break;
				case 2:
					usable = new RepairKit(DefaultConstants.REPAIR_KIT_WEIGHT.multiply(randomGenerator.nextDouble()),
							DefaultConstants.REPAIR_KIT_ENERGY_LIMIT.multiply(randomGenerator.nextDouble()), null, robot.getPosition());
					break;
				default:
					throw new Error();
				}
				robot.addAsUsableItem(usable);
				super.execute(robot, surpriseBox);
			}
			
		};
		
		/**
		 * Executes the behavior of the given surpriseBox.
		 * 
		 * @param robot The robot that will use this surpriseBox.
		 * @param surpriseBox The surpriseBox that will be used by the robot.
		 * @pre
		 * 		| robot != null
		 * @pre
		 * 		| robot.canUse(surpriseBox)
		 * @pre
		 * 		| !surpriseBox.isTerminated()
		 * @effect
		 * 		| surpriseBox.terminate()
		 */
		public void execute(Robot robot, SurpriseBox surpriseBox) {
			assert robot != null;
			assert robot.canUse(surpriseBox);
			assert !surpriseBox.isTerminated();
			surpriseBox.terminate();
		}
	}

}
